package lightclient

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"math/rand"
	"net/http"
	"net/http/httptest"
	"strconv"
	"testing"

	"github.com/OffchainLabs/prysm/v7/api/server/structs"
	"github.com/OffchainLabs/prysm/v7/async/event"
	blockchainTest "github.com/OffchainLabs/prysm/v7/beacon-chain/blockchain/testing"
	"github.com/OffchainLabs/prysm/v7/beacon-chain/core/helpers"
	"github.com/OffchainLabs/prysm/v7/beacon-chain/db"
	dbtesting "github.com/OffchainLabs/prysm/v7/beacon-chain/db/testing"
	lightclient "github.com/OffchainLabs/prysm/v7/beacon-chain/light-client"
	p2ptesting "github.com/OffchainLabs/prysm/v7/beacon-chain/p2p/testing"
	fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams"
	"github.com/OffchainLabs/prysm/v7/config/params"
	"github.com/OffchainLabs/prysm/v7/consensus-types/blocks"
	"github.com/OffchainLabs/prysm/v7/consensus-types/interfaces"
	light_client "github.com/OffchainLabs/prysm/v7/consensus-types/light-client"
	"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
	enginev1 "github.com/OffchainLabs/prysm/v7/proto/engine/v1"
	pb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
	"github.com/OffchainLabs/prysm/v7/runtime/version"
	"github.com/OffchainLabs/prysm/v7/testing/require"
	"github.com/OffchainLabs/prysm/v7/testing/util"
	"github.com/ethereum/go-ethereum/common/hexutil"
	ssz "github.com/prysmaticlabs/fastssz"
	"google.golang.org/protobuf/proto"
)

func TestLightClientHandler_GetLightClientBootstrap(t *testing.T) {
	params.SetupTestConfigCleanup(t)
	cfg := params.BeaconConfig()
	cfg.AltairForkEpoch = 0
	cfg.BellatrixForkEpoch = 1
	cfg.CapellaForkEpoch = 2
	cfg.DenebForkEpoch = 3
	cfg.ElectraForkEpoch = 4
	cfg.FuluForkEpoch = 5
	params.OverrideBeaconConfig(cfg)

	for _, testVersion := range version.All()[1:] {
		if testVersion == version.Gloas {
			// TODO(16027): Unskip light client tests for Gloas
			continue
		}
		t.Run(version.String(testVersion), func(t *testing.T) {
			l := util.NewTestLightClient(t, testVersion)

			slot := primitives.Slot(params.BeaconConfig().VersionToForkEpochMap()[testVersion] * primitives.Epoch(params.BeaconConfig().SlotsPerEpoch)).Add(1)
			blockRoot, err := l.Block.Block().HashTreeRoot()
			require.NoError(t, err)

			bootstrap, err := lightclient.NewLightClientBootstrapFromBeaconState(l.Ctx, slot, l.State, l.Block)
			require.NoError(t, err)

			db := dbtesting.SetupDB(t)
			lcStore := lightclient.NewLightClientStore(&p2ptesting.FakeP2P{}, new(event.Feed), db)
			require.NoError(t, err)

			err = db.SaveLightClientBootstrap(l.Ctx, blockRoot[:], bootstrap)
			require.NoError(t, err)

			s := &Server{
				LCStore: lcStore,
			}
			request := httptest.NewRequest("GET", "http://foo.com/", nil)
			request.SetPathValue("block_root", hexutil.Encode(blockRoot[:]))
			writer := httptest.NewRecorder()
			writer.Body = &bytes.Buffer{}

			s.GetLightClientBootstrap(writer, request)
			require.Equal(t, http.StatusOK, writer.Code)

			var resp structs.LightClientBootstrapResponse
			err = json.Unmarshal(writer.Body.Bytes(), &resp)
			require.NoError(t, err)
			var respHeader structs.LightClientHeader
			err = json.Unmarshal(resp.Data.Header, &respHeader)
			require.NoError(t, err)
			require.Equal(t, version.String(testVersion), resp.Version)

			blockHeader, err := l.Block.Header()
			require.NoError(t, err)
			require.Equal(t, hexutil.Encode(blockHeader.Header.BodyRoot), respHeader.Beacon.BodyRoot)
			require.Equal(t, strconv.FormatUint(uint64(blockHeader.Header.Slot), 10), respHeader.Beacon.Slot)

			require.NotNil(t, resp.Data.CurrentSyncCommittee)
			require.NotNil(t, resp.Data.CurrentSyncCommitteeBranch)
		})

		t.Run(version.String(testVersion)+"SSZ", func(t *testing.T) {
			l := util.NewTestLightClient(t, testVersion)

			slot := primitives.Slot(params.BeaconConfig().VersionToForkEpochMap()[testVersion] * primitives.Epoch(params.BeaconConfig().SlotsPerEpoch)).Add(1)
			blockRoot, err := l.Block.Block().HashTreeRoot()
			require.NoError(t, err)

			bootstrap, err := lightclient.NewLightClientBootstrapFromBeaconState(l.Ctx, slot, l.State, l.Block)
			require.NoError(t, err)

			db := dbtesting.SetupDB(t)
			lcStore := lightclient.NewLightClientStore(&p2ptesting.FakeP2P{}, new(event.Feed), db)
			require.NoError(t, err)

			err = db.SaveLightClientBootstrap(l.Ctx, blockRoot[:], bootstrap)
			require.NoError(t, err)

			s := &Server{
				LCStore: lcStore,
			}
			request := httptest.NewRequest("GET", "http://foo.com/", nil)
			request.SetPathValue("block_root", hexutil.Encode(blockRoot[:]))
			request.Header.Add("Accept", "application/octet-stream")
			writer := httptest.NewRecorder()
			writer.Body = &bytes.Buffer{}

			s.GetLightClientBootstrap(writer, request)
			require.Equal(t, http.StatusOK, writer.Code)

			var resp proto.Message
			switch testVersion {
			case version.Altair:
				resp = &pb.LightClientBootstrapAltair{}
			case version.Bellatrix:
				resp = &pb.LightClientBootstrapAltair{}
			case version.Capella:
				resp = &pb.LightClientBootstrapCapella{}
			case version.Deneb:
				resp = &pb.LightClientBootstrapDeneb{}
			case version.Electra, version.Fulu:
				resp = &pb.LightClientBootstrapElectra{}
			default:
				t.Fatalf("Unsupported version %s", version.String(testVersion))
			}
			obj := resp.(ssz.Unmarshaler)
			err = obj.UnmarshalSSZ(writer.Body.Bytes())
			require.NoError(t, err)

			bootstrapSSZ, err := bootstrap.MarshalSSZ()
			require.NoError(t, err)
			require.DeepSSZEqual(t, bootstrapSSZ, writer.Body.Bytes())
		})
	}

	t.Run("no bootstrap found", func(t *testing.T) {
		lcStore := lightclient.NewLightClientStore(&p2ptesting.FakeP2P{}, new(event.Feed), dbtesting.SetupDB(t))
		s := &Server{
			LCStore: lcStore,
		}
		request := httptest.NewRequest("GET", "http://foo.com/", nil)
		request.SetPathValue("block_root", hexutil.Encode([]byte{0x00, 0x01, 0x02}))
		writer := httptest.NewRecorder()
		writer.Body = &bytes.Buffer{}

		s.GetLightClientBootstrap(writer, request)
		require.Equal(t, http.StatusNotFound, writer.Code)
	})
}

func TestLightClientHandler_GetLightClientByRange(t *testing.T) {
	helpers.ClearCache()
	ctx := t.Context()

	params.SetupTestConfigCleanup(t)
	config := params.BeaconConfig()
	config.EpochsPerSyncCommitteePeriod = 1
	config.AltairForkEpoch = 0
	config.BellatrixForkEpoch = 1
	config.CapellaForkEpoch = 2
	config.DenebForkEpoch = 3
	config.ElectraForkEpoch = 4
	config.FuluForkEpoch = 5
	params.OverrideBeaconConfig(config)

	t.Run("can save retrieve", func(t *testing.T) {
		for _, testVersion := range version.All()[1:] {
			if testVersion == version.Gloas {
				// TODO(16027): Unskip light client tests for Gloas
				continue
			}
			t.Run(version.String(testVersion), func(t *testing.T) {

				slot := primitives.Slot(params.BeaconConfig().VersionToForkEpochMap()[testVersion] * primitives.Epoch(config.SlotsPerEpoch)).Add(1)
				startPeriod := uint64(slot.Div(uint64(config.EpochsPerSyncCommitteePeriod)).Div(uint64(config.SlotsPerEpoch)))

				db := dbtesting.SetupDB(t)

				updates := make([]interfaces.LightClientUpdate, 0)
				for i := 1; i <= 2; i++ {
					update, err := createUpdate(t, testVersion)
					require.NoError(t, err)
					updates = append(updates, update)
				}

				lcStore := lightclient.NewLightClientStore(&p2ptesting.FakeP2P{}, new(event.Feed), db)

				blk := util.NewBeaconBlock()
				signedBlk, err := blocks.NewSignedBeaconBlock(blk)
				require.NoError(t, err)

				s := &Server{
					LCStore: lcStore,
					HeadFetcher: &blockchainTest.ChainService{
						Block: signedBlk,
					},
				}

				saveHead(t, ctx, db)

				updatePeriod := startPeriod
				for _, update := range updates {
					err := db.SaveLightClientUpdate(ctx, updatePeriod, update)
					require.NoError(t, err)
					updatePeriod++
				}

				t.Run("single update", func(t *testing.T) {
					url := fmt.Sprintf("http://foo.com/?count=1&start_period=%d", startPeriod)
					request := httptest.NewRequest("GET", url, nil)
					writer := httptest.NewRecorder()
					writer.Body = &bytes.Buffer{}

					s.GetLightClientUpdatesByRange(writer, request)

					require.Equal(t, http.StatusOK, writer.Code)
					var resp structs.LightClientUpdatesByRangeResponse
					err := json.Unmarshal(writer.Body.Bytes(), &resp.Updates)
					require.NoError(t, err)
					require.Equal(t, 1, len(resp.Updates))
					require.Equal(t, version.String(testVersion), resp.Updates[0].Version)
					updateJson, err := structs.LightClientUpdateFromConsensus(updates[0])
					require.NoError(t, err)
					require.DeepEqual(t, updateJson, resp.Updates[0].Data)
				})

				t.Run("single update ssz", func(t *testing.T) {
					url := fmt.Sprintf("http://foo.com/?count=1&start_period=%d", startPeriod)
					request := httptest.NewRequest("GET", url, nil)
					request.Header.Add("Accept", "application/octet-stream")
					writer := httptest.NewRecorder()
					writer.Body = &bytes.Buffer{}

					s.GetLightClientUpdatesByRange(writer, request)

					require.Equal(t, http.StatusOK, writer.Code)
					var resp proto.Message
					switch testVersion {
					case version.Altair:
						resp = &pb.LightClientUpdateAltair{}
					case version.Bellatrix:
						resp = &pb.LightClientUpdateAltair{}
					case version.Capella:
						resp = &pb.LightClientUpdateCapella{}
					case version.Deneb:
						resp = &pb.LightClientUpdateDeneb{}
					case version.Electra, version.Fulu:
						resp = &pb.LightClientUpdateElectra{}
					default:
						t.Fatalf("Unsupported version %s", version.String(testVersion))
					}
					obj := resp.(ssz.Unmarshaler)
					err := obj.UnmarshalSSZ(writer.Body.Bytes()[12:]) // skip the length and fork digest prefixes
					require.NoError(t, err)

					ussz, err := updates[0].MarshalSSZ()
					require.NoError(t, err)
					require.DeepSSZEqual(t, ussz, writer.Body.Bytes()[12:])
				})

				t.Run("multiple updates", func(t *testing.T) {
					url := fmt.Sprintf("http://foo.com/?count=100&start_period=%d", startPeriod)
					request := httptest.NewRequest("GET", url, nil)
					writer := httptest.NewRecorder()
					writer.Body = &bytes.Buffer{}

					s.GetLightClientUpdatesByRange(writer, request)

					require.Equal(t, http.StatusOK, writer.Code)
					var resp structs.LightClientUpdatesByRangeResponse
					err := json.Unmarshal(writer.Body.Bytes(), &resp.Updates)
					require.NoError(t, err)
					require.Equal(t, 2, len(resp.Updates))
					for i, update := range updates {
						require.Equal(t, version.String(testVersion), resp.Updates[i].Version)
						updateJson, err := structs.LightClientUpdateFromConsensus(update)
						require.NoError(t, err)
						require.DeepEqual(t, updateJson, resp.Updates[i].Data)
					}
				})

				t.Run("multiple updates ssz", func(t *testing.T) {
					url := fmt.Sprintf("http://foo.com/?count=100&start_period=%d", startPeriod)
					request := httptest.NewRequest("GET", url, nil)
					request.Header.Add("Accept", "application/octet-stream")
					writer := httptest.NewRecorder()
					writer.Body = &bytes.Buffer{}

					s.GetLightClientUpdatesByRange(writer, request)

					require.Equal(t, http.StatusOK, writer.Code)

					offset := 0
					for i := 0; offset < writer.Body.Len(); i++ {
						updateLen := int(ssz.UnmarshallUint64(writer.Body.Bytes()[offset:offset+8]) - 4)
						offset += 12

						var resp proto.Message
						switch testVersion {
						case version.Altair:
							resp = &pb.LightClientUpdateAltair{}
						case version.Bellatrix:
							resp = &pb.LightClientUpdateAltair{}
						case version.Capella:
							resp = &pb.LightClientUpdateCapella{}
						case version.Deneb:
							resp = &pb.LightClientUpdateDeneb{}
						case version.Electra, version.Fulu:
							resp = &pb.LightClientUpdateElectra{}
						default:
							t.Fatalf("Unsupported version %s", version.String(testVersion))
						}
						obj := resp.(ssz.Unmarshaler)

						updateBytes := writer.Body.Bytes()[offset : offset+updateLen]

						err := obj.UnmarshalSSZ(updateBytes)
						require.NoError(t, err)

						ussz, err := updates[i].MarshalSSZ()
						require.NoError(t, err)
						require.DeepSSZEqual(t, ussz, updateBytes)

						offset += updateLen
					}
				})
			})
		}
	})

	t.Run("updates from multiple forks", func(t *testing.T) {
		for testVersion := version.Altair; testVersion < version.Electra; testVersion++ { // 1-2, 2-3, 3-4, 4-5
			t.Run(version.String(testVersion)+"-"+version.String(testVersion+1), func(t *testing.T) {
				firstForkSlot := primitives.Slot(params.BeaconConfig().VersionToForkEpochMap()[testVersion] * primitives.Epoch(config.SlotsPerEpoch)).Add(1)
				secondForkSlot := primitives.Slot(params.BeaconConfig().VersionToForkEpochMap()[testVersion+1] * primitives.Epoch(config.SlotsPerEpoch)).Add(1)

				db := dbtesting.SetupDB(t)
				lcStore := lightclient.NewLightClientStore(&p2ptesting.FakeP2P{}, new(event.Feed), db)

				blk := util.NewBeaconBlock()
				signedBlk, err := blocks.NewSignedBeaconBlock(blk)
				require.NoError(t, err)

				s := &Server{
					LCStore: lcStore,
					HeadFetcher: &blockchainTest.ChainService{
						Block: signedBlk,
					},
				}

				saveHead(t, ctx, db)

				updates := make([]interfaces.LightClientUpdate, 2)

				updatePeriod := firstForkSlot.Div(uint64(config.EpochsPerSyncCommitteePeriod)).Div(uint64(config.SlotsPerEpoch))
				startPeriod := updatePeriod

				updates[0], err = createUpdate(t, testVersion)
				require.NoError(t, err)

				err = db.SaveLightClientUpdate(ctx, uint64(updatePeriod), updates[0])
				require.NoError(t, err)

				updatePeriod = secondForkSlot.Div(uint64(config.EpochsPerSyncCommitteePeriod)).Div(uint64(config.SlotsPerEpoch))

				updates[1], err = createUpdate(t, testVersion+1)
				require.NoError(t, err)

				err = db.SaveLightClientUpdate(ctx, uint64(updatePeriod), updates[1])
				require.NoError(t, err)

				t.Run("json", func(t *testing.T) {
					url := fmt.Sprintf("http://foo.com/?count=100&start_period=%d", startPeriod)
					request := httptest.NewRequest("GET", url, nil)
					writer := httptest.NewRecorder()
					writer.Body = &bytes.Buffer{}

					s.GetLightClientUpdatesByRange(writer, request)

					require.Equal(t, http.StatusOK, writer.Code)
					var resp structs.LightClientUpdatesByRangeResponse
					err = json.Unmarshal(writer.Body.Bytes(), &resp.Updates)
					require.NoError(t, err)
					require.Equal(t, 2, len(resp.Updates))
					for i, update := range updates {
						if i < 1 {
							require.Equal(t, version.String(testVersion), resp.Updates[i].Version)
						} else {
							require.Equal(t, version.String(testVersion+1), resp.Updates[i].Version)
						}
						updateJson, err := structs.LightClientUpdateFromConsensus(update)
						require.NoError(t, err)
						require.DeepEqual(t, updateJson, resp.Updates[i].Data)
					}
				})

				t.Run("ssz", func(t *testing.T) {
					url := fmt.Sprintf("http://foo.com/?count=100&start_period=%d", startPeriod)
					request := httptest.NewRequest("GET", url, nil)
					request.Header.Add("Accept", "application/octet-stream")
					writer := httptest.NewRecorder()
					writer.Body = &bytes.Buffer{}

					s.GetLightClientUpdatesByRange(writer, request)

					require.Equal(t, http.StatusOK, writer.Code)

					offset := 0
					updateLen := int(ssz.UnmarshallUint64(writer.Body.Bytes()[offset:offset+8]) - 4)
					offset += 12
					var resp proto.Message
					switch testVersion {
					case version.Altair:
						resp = &pb.LightClientUpdateAltair{}
					case version.Bellatrix:
						resp = &pb.LightClientUpdateAltair{}
					case version.Capella:
						resp = &pb.LightClientUpdateCapella{}
					case version.Deneb:
						resp = &pb.LightClientUpdateDeneb{}
					case version.Electra:
						resp = &pb.LightClientUpdateElectra{}
					default:
						t.Fatalf("Unsupported version %s", version.String(testVersion))
					}
					obj := resp.(ssz.Unmarshaler)
					err = obj.UnmarshalSSZ(writer.Body.Bytes()[offset : offset+updateLen])
					require.NoError(t, err)
					u0ssz, err := updates[0].MarshalSSZ()
					require.NoError(t, err)
					require.DeepSSZEqual(t, u0ssz, writer.Body.Bytes()[offset:offset+updateLen])

					offset += updateLen
					updateLen = int(ssz.UnmarshallUint64(writer.Body.Bytes()[offset:offset+8]) - 4)
					offset += 12
					var resp1 proto.Message
					switch testVersion + 1 {
					case version.Altair:
						resp1 = &pb.LightClientUpdateAltair{}
					case version.Bellatrix:
						resp1 = &pb.LightClientUpdateAltair{}
					case version.Capella:
						resp1 = &pb.LightClientUpdateCapella{}
					case version.Deneb:
						resp1 = &pb.LightClientUpdateDeneb{}
					case version.Electra:
						resp1 = &pb.LightClientUpdateElectra{}
					default:
						t.Fatalf("Unsupported version %s", version.String(testVersion+1))
					}
					obj1 := resp1.(ssz.Unmarshaler)
					err = obj1.UnmarshalSSZ(writer.Body.Bytes()[offset : offset+updateLen])
					require.NoError(t, err)
					u1ssz, err := updates[1].MarshalSSZ()
					require.NoError(t, err)
					require.DeepSSZEqual(t, u1ssz, writer.Body.Bytes()[offset:offset+updateLen])
				})
			})
		}
	})

	t.Run("count bigger than limit", func(t *testing.T) {
		config.MaxRequestLightClientUpdates = 2
		params.OverrideBeaconConfig(config)
		slot := primitives.Slot(config.AltairForkEpoch * primitives.Epoch(config.SlotsPerEpoch)).Add(1)

		db := dbtesting.SetupDB(t)
		lcStore := lightclient.NewLightClientStore(&p2ptesting.FakeP2P{}, new(event.Feed), db)

		blk := util.NewBeaconBlock()
		signedBlk, err := blocks.NewSignedBeaconBlock(blk)
		require.NoError(t, err)

		s := &Server{
			LCStore: lcStore,
			HeadFetcher: &blockchainTest.ChainService{
				Block: signedBlk,
			},
		}

		saveHead(t, ctx, db)

		updates := make([]interfaces.LightClientUpdate, 3)

		updatePeriod := slot.Div(uint64(config.EpochsPerSyncCommitteePeriod)).Div(uint64(config.SlotsPerEpoch))

		for i := range 3 {
			updates[i], err = createUpdate(t, version.Altair)
			require.NoError(t, err)

			err = db.SaveLightClientUpdate(ctx, uint64(updatePeriod), updates[i])
			require.NoError(t, err)

			updatePeriod++
		}

		startPeriod := 0
		url := fmt.Sprintf("http://foo.com/?count=4&start_period=%d", startPeriod)
		request := httptest.NewRequest("GET", url, nil)
		writer := httptest.NewRecorder()
		writer.Body = &bytes.Buffer{}

		s.GetLightClientUpdatesByRange(writer, request)

		require.Equal(t, http.StatusOK, writer.Code)
		var resp structs.LightClientUpdatesByRangeResponse
		err = json.Unmarshal(writer.Body.Bytes(), &resp.Updates)
		require.NoError(t, err)
		require.Equal(t, 2, len(resp.Updates))
		for i, update := range updates {
			if i < 2 {
				require.Equal(t, "altair", resp.Updates[i].Version)
				updateJson, err := structs.LightClientUpdateFromConsensus(update)
				require.NoError(t, err)
				require.DeepEqual(t, updateJson, resp.Updates[i].Data)
			}
		}
	})

	t.Run("count bigger than max", func(t *testing.T) {
		config.MaxRequestLightClientUpdates = 2
		params.OverrideBeaconConfig(config)
		slot := primitives.Slot(config.AltairForkEpoch * primitives.Epoch(config.SlotsPerEpoch)).Add(1)

		db := dbtesting.SetupDB(t)
		lcStore := lightclient.NewLightClientStore(&p2ptesting.FakeP2P{}, new(event.Feed), db)

		blk := util.NewBeaconBlock()
		signedBlk, err := blocks.NewSignedBeaconBlock(blk)
		require.NoError(t, err)

		s := &Server{
			LCStore: lcStore,
			HeadFetcher: &blockchainTest.ChainService{
				Block: signedBlk,
			},
		}

		saveHead(t, ctx, db)

		updates := make([]interfaces.LightClientUpdate, 3)

		updatePeriod := slot.Div(uint64(config.EpochsPerSyncCommitteePeriod)).Div(uint64(config.SlotsPerEpoch))

		for i := range 3 {
			updates[i], err = createUpdate(t, version.Altair)
			require.NoError(t, err)

			err = db.SaveLightClientUpdate(ctx, uint64(updatePeriod), updates[i])
			require.NoError(t, err)

			updatePeriod++
		}

		startPeriod := 0
		url := fmt.Sprintf("http://foo.com/?count=10&start_period=%d", startPeriod)
		request := httptest.NewRequest("GET", url, nil)
		writer := httptest.NewRecorder()
		writer.Body = &bytes.Buffer{}

		s.GetLightClientUpdatesByRange(writer, request)

		require.Equal(t, http.StatusOK, writer.Code)
		var resp structs.LightClientUpdatesByRangeResponse
		err = json.Unmarshal(writer.Body.Bytes(), &resp.Updates)
		require.NoError(t, err)
		require.Equal(t, 2, len(resp.Updates))
		for i, update := range updates {
			if i < 2 {
				require.Equal(t, "altair", resp.Updates[i].Version)
				updateJson, err := structs.LightClientUpdateFromConsensus(update)
				require.NoError(t, err)
				require.DeepEqual(t, updateJson, resp.Updates[i].Data)
			}
		}
	})

	t.Run("start period before altair", func(t *testing.T) {
		config.AltairForkEpoch = 1
		params.OverrideBeaconConfig(config)

		db := dbtesting.SetupDB(t)
		lcStore := lightclient.NewLightClientStore(&p2ptesting.FakeP2P{}, new(event.Feed), db)

		s := &Server{
			LCStore: lcStore,
		}

		startPeriod := 0
		url := fmt.Sprintf("http://foo.com/?count=128&start_period=%d", startPeriod)
		request := httptest.NewRequest("GET", url, nil)
		writer := httptest.NewRecorder()
		writer.Body = &bytes.Buffer{}

		s.GetLightClientUpdatesByRange(writer, request)

		require.Equal(t, http.StatusBadRequest, writer.Code)

		config.AltairForkEpoch = 0
		params.OverrideBeaconConfig(config)
	})

	t.Run("missing updates", func(t *testing.T) {
		slot := primitives.Slot(config.AltairForkEpoch * primitives.Epoch(config.SlotsPerEpoch)).Add(1)

		t.Run("missing update in the middle", func(t *testing.T) {
			db := dbtesting.SetupDB(t)
			lcStore := lightclient.NewLightClientStore(&p2ptesting.FakeP2P{}, new(event.Feed), db)

			blk := util.NewBeaconBlock()
			signedBlk, err := blocks.NewSignedBeaconBlock(blk)
			require.NoError(t, err)

			s := &Server{
				LCStore: lcStore,
				HeadFetcher: &blockchainTest.ChainService{
					Block: signedBlk,
				},
			}

			saveHead(t, ctx, db)

			updates := make([]interfaces.LightClientUpdate, 3)

			updatePeriod := slot.Div(uint64(config.EpochsPerSyncCommitteePeriod)).Div(uint64(config.SlotsPerEpoch))

			for i := range 3 {
				if i == 1 { // skip this update
					updatePeriod++
					continue
				}
				updates[i], err = createUpdate(t, version.Altair)
				require.NoError(t, err)

				err = db.SaveLightClientUpdate(ctx, uint64(updatePeriod), updates[i])
				require.NoError(t, err)

				updatePeriod++
			}

			startPeriod := 0
			url := fmt.Sprintf("http://foo.com/?count=10&start_period=%d", startPeriod)
			request := httptest.NewRequest("GET", url, nil)
			writer := httptest.NewRecorder()
			writer.Body = &bytes.Buffer{}

			s.GetLightClientUpdatesByRange(writer, request)

			require.Equal(t, http.StatusOK, writer.Code)
			var resp structs.LightClientUpdatesByRangeResponse
			err = json.Unmarshal(writer.Body.Bytes(), &resp.Updates)
			require.NoError(t, err)
			require.Equal(t, 1, len(resp.Updates))
			require.Equal(t, "altair", resp.Updates[0].Version)
			updateJson, err := structs.LightClientUpdateFromConsensus(updates[0])
			require.NoError(t, err)
			require.DeepEqual(t, updateJson, resp.Updates[0].Data)
		})

		t.Run("missing update at the beginning", func(t *testing.T) {
			db := dbtesting.SetupDB(t)
			lcStore := lightclient.NewLightClientStore(&p2ptesting.FakeP2P{}, new(event.Feed), db)

			blk := util.NewBeaconBlock()
			signedBlk, err := blocks.NewSignedBeaconBlock(blk)
			require.NoError(t, err)

			s := &Server{
				LCStore: lcStore,
				HeadFetcher: &blockchainTest.ChainService{
					Block: signedBlk,
				},
			}

			saveHead(t, ctx, db)

			updates := make([]interfaces.LightClientUpdate, 3)

			updatePeriod := slot.Div(uint64(config.EpochsPerSyncCommitteePeriod)).Div(uint64(config.SlotsPerEpoch))

			for i := range 3 {
				if i == 0 { // skip this update
					updatePeriod++
					continue
				}

				updates[i], err = createUpdate(t, version.Altair)
				require.NoError(t, err)

				err = db.SaveLightClientUpdate(ctx, uint64(updatePeriod), updates[i])
				require.NoError(t, err)

				updatePeriod++
			}

			startPeriod := 0
			url := fmt.Sprintf("http://foo.com/?count=10&start_period=%d", startPeriod)
			request := httptest.NewRequest("GET", url, nil)
			writer := httptest.NewRecorder()
			writer.Body = &bytes.Buffer{}

			s.GetLightClientUpdatesByRange(writer, request)

			require.Equal(t, http.StatusOK, writer.Code)
			var resp structs.LightClientUpdatesByRangeResponse
			err = json.Unmarshal(writer.Body.Bytes(), &resp.Updates)
			require.NoError(t, err)
			require.Equal(t, 0, len(resp.Updates))
		})
	})
}

func TestLightClientHandler_GetLightClientFinalityUpdate(t *testing.T) {
	helpers.ClearCache()

	t.Run("no update", func(t *testing.T) {
		s := &Server{LCStore: &lightclient.Store{}}
		request := httptest.NewRequest("GET", "http://foo.com", nil)
		writer := httptest.NewRecorder()
		writer.Body = &bytes.Buffer{}
		s.GetLightClientFinalityUpdate(writer, request)
		require.Equal(t, http.StatusNotFound, writer.Code)
	})

	for _, testVersion := range version.All()[1:] {
		if testVersion == version.Gloas {
			// TODO(16027): Unskip light client tests for Gloas
			continue
		}
		t.Run(version.String(testVersion), func(t *testing.T) {
			ctx := t.Context()

			l := util.NewTestLightClient(t, testVersion)
			update, err := lightclient.NewLightClientFinalityUpdateFromBeaconState(ctx, l.State, l.Block, l.AttestedState, l.AttestedBlock, l.FinalizedBlock)
			require.NoError(t, err)

			lcStore := lightclient.NewLightClientStore(&p2ptesting.FakeP2P{}, new(event.Feed), dbtesting.SetupDB(t))
			require.NoError(t, err)

			s := &Server{
				LCStore: lcStore,
			}
			s.LCStore.SetLastFinalityUpdate(update, false)

			request := httptest.NewRequest("GET", "http://foo.com", nil)
			writer := httptest.NewRecorder()
			writer.Body = &bytes.Buffer{}
			s.GetLightClientFinalityUpdate(writer, request)
			require.Equal(t, http.StatusOK, writer.Code)

			data, err := structs.LightClientFinalityUpdateFromConsensus(update)
			require.NoError(t, err)
			var resp structs.LightClientFinalityUpdateResponse
			err = json.Unmarshal(writer.Body.Bytes(), &resp)
			require.NoError(t, err)
			require.Equal(t, version.String(testVersion), resp.Version)
			require.DeepEqual(t, data, resp.Data)
		})

		t.Run(version.String(testVersion)+" SSZ", func(t *testing.T) {
			ctx := t.Context()

			l := util.NewTestLightClient(t, testVersion)
			update, err := lightclient.NewLightClientFinalityUpdateFromBeaconState(ctx, l.State, l.Block, l.AttestedState, l.AttestedBlock, l.FinalizedBlock)
			require.NoError(t, err)

			lcStore := lightclient.NewLightClientStore(&p2ptesting.FakeP2P{}, new(event.Feed), dbtesting.SetupDB(t))
			require.NoError(t, err)

			s := &Server{
				LCStore: lcStore,
			}
			s.LCStore.SetLastFinalityUpdate(update, false)

			request := httptest.NewRequest("GET", "http://foo.com", nil)
			request.Header.Add("Accept", "application/octet-stream")
			writer := httptest.NewRecorder()
			writer.Body = &bytes.Buffer{}
			s.GetLightClientFinalityUpdate(writer, request)
			require.Equal(t, http.StatusOK, writer.Code)

			var resp proto.Message
			switch testVersion {
			case version.Altair:
				resp = &pb.LightClientFinalityUpdateAltair{}
			case version.Bellatrix:
				resp = &pb.LightClientFinalityUpdateAltair{}
			case version.Capella:
				resp = &pb.LightClientFinalityUpdateCapella{}
			case version.Deneb:
				resp = &pb.LightClientFinalityUpdateDeneb{}
			case version.Electra, version.Fulu:
				resp = &pb.LightClientFinalityUpdateElectra{}
			default:
				t.Fatalf("Unsupported version %s", version.String(testVersion))
			}
			obj := resp.(ssz.Unmarshaler)
			err = obj.UnmarshalSSZ(writer.Body.Bytes())
			require.NoError(t, err)
			updateSSZ, err := update.MarshalSSZ()
			require.NoError(t, err)
			require.DeepSSZEqual(t, updateSSZ, writer.Body.Bytes())
		})
	}
}

func TestLightClientHandler_GetLightClientOptimisticUpdate(t *testing.T) {
	helpers.ClearCache()

	t.Run("no update", func(t *testing.T) {
		lcStore := lightclient.NewLightClientStore(&p2ptesting.FakeP2P{}, new(event.Feed), dbtesting.SetupDB(t))

		s := &Server{
			LCStore: lcStore,
		}

		request := httptest.NewRequest("GET", "http://foo.com", nil)
		writer := httptest.NewRecorder()
		writer.Body = &bytes.Buffer{}
		s.GetLightClientOptimisticUpdate(writer, request)
		require.Equal(t, http.StatusNotFound, writer.Code)
	})

	for _, testVersion := range version.All()[1:] {
		if testVersion == version.Gloas {
			// TODO(16027): Unskip light client tests for Gloas
			continue
		}
		t.Run(version.String(testVersion), func(t *testing.T) {
			ctx := t.Context()
			l := util.NewTestLightClient(t, testVersion)
			update, err := lightclient.NewLightClientOptimisticUpdateFromBeaconState(ctx, l.State, l.Block, l.AttestedState, l.AttestedBlock)
			require.NoError(t, err)

			lcStore := lightclient.NewLightClientStore(&p2ptesting.FakeP2P{}, new(event.Feed), dbtesting.SetupDB(t))
			require.NoError(t, err)

			s := &Server{
				LCStore: lcStore,
			}
			s.LCStore.SetLastOptimisticUpdate(update, false)

			request := httptest.NewRequest("GET", "http://foo.com", nil)
			writer := httptest.NewRecorder()
			writer.Body = &bytes.Buffer{}
			s.GetLightClientOptimisticUpdate(writer, request)
			require.Equal(t, http.StatusOK, writer.Code)

			data, err := structs.LightClientOptimisticUpdateFromConsensus(update)
			require.NoError(t, err)
			var resp structs.LightClientOptimisticUpdateResponse
			err = json.Unmarshal(writer.Body.Bytes(), &resp)
			require.NoError(t, err)
			require.Equal(t, version.String(testVersion), resp.Version)
			require.DeepEqual(t, data, resp.Data)
		})

		t.Run(version.String(testVersion)+" SSZ", func(t *testing.T) {
			ctx := t.Context()
			l := util.NewTestLightClient(t, testVersion)
			update, err := lightclient.NewLightClientOptimisticUpdateFromBeaconState(ctx, l.State, l.Block, l.AttestedState, l.AttestedBlock)
			require.NoError(t, err)

			lcStore := lightclient.NewLightClientStore(&p2ptesting.FakeP2P{}, new(event.Feed), dbtesting.SetupDB(t))
			require.NoError(t, err)

			s := &Server{
				LCStore: lcStore,
			}
			s.LCStore.SetLastOptimisticUpdate(update, false)

			request := httptest.NewRequest("GET", "http://foo.com", nil)
			request.Header.Add("Accept", "application/octet-stream")
			writer := httptest.NewRecorder()
			writer.Body = &bytes.Buffer{}
			s.GetLightClientOptimisticUpdate(writer, request)
			require.Equal(t, http.StatusOK, writer.Code)

			var resp proto.Message
			switch testVersion {
			case version.Altair:
				resp = &pb.LightClientOptimisticUpdateAltair{}
			case version.Bellatrix:
				resp = &pb.LightClientOptimisticUpdateAltair{}
			case version.Capella:
				resp = &pb.LightClientOptimisticUpdateCapella{}
			case version.Deneb:
				resp = &pb.LightClientOptimisticUpdateDeneb{}
			case version.Electra, version.Fulu:
				resp = &pb.LightClientOptimisticUpdateDeneb{}
			default:
				t.Fatalf("Unsupported version %s", version.String(testVersion))
			}
			obj := resp.(ssz.Unmarshaler)
			err = obj.UnmarshalSSZ(writer.Body.Bytes())
			require.NoError(t, err)
			updateSSZ, err := update.MarshalSSZ()
			require.NoError(t, err)
			require.DeepSSZEqual(t, updateSSZ, writer.Body.Bytes())
		})
	}
}

func createUpdate(t *testing.T, v int) (interfaces.LightClientUpdate, error) {
	config := params.BeaconConfig()
	var slot primitives.Slot
	var header interfaces.LightClientHeader
	var blk interfaces.ReadOnlySignedBeaconBlock
	var err error

	sampleRoot := make([]byte, 32)
	for i := range 32 {
		sampleRoot[i] = byte(i)
	}

	sampleExecutionBranch := make([][]byte, fieldparams.ExecutionBranchDepth)
	for i := range 4 {
		sampleExecutionBranch[i] = make([]byte, 32)
		for j := range 32 {
			sampleExecutionBranch[i][j] = byte(i + j)
		}
	}

	switch v {
	case version.Altair:
		slot = primitives.Slot(config.AltairForkEpoch * primitives.Epoch(config.SlotsPerEpoch)).Add(1)
		header, err = light_client.NewWrappedHeader(&pb.LightClientHeaderAltair{
			Beacon: &pb.BeaconBlockHeader{
				Slot:          slot,
				ProposerIndex: primitives.ValidatorIndex(rand.Int()),
				ParentRoot:    sampleRoot,
				StateRoot:     sampleRoot,
				BodyRoot:      sampleRoot,
			},
		})
		require.NoError(t, err)
		blk, err = blocks.NewSignedBeaconBlock(util.NewBeaconBlockAltair())
		require.NoError(t, err)
	case version.Bellatrix:
		slot = primitives.Slot(config.BellatrixForkEpoch * primitives.Epoch(config.SlotsPerEpoch)).Add(1)
		header, err = light_client.NewWrappedHeader(&pb.LightClientHeaderAltair{
			Beacon: &pb.BeaconBlockHeader{
				Slot:          slot,
				ProposerIndex: primitives.ValidatorIndex(rand.Int()),
				ParentRoot:    sampleRoot,
				StateRoot:     sampleRoot,
				BodyRoot:      sampleRoot,
			},
		})
		require.NoError(t, err)
		blk, err = blocks.NewSignedBeaconBlock(util.NewBeaconBlockBellatrix())
		require.NoError(t, err)
	case version.Capella:
		slot = primitives.Slot(config.CapellaForkEpoch * primitives.Epoch(config.SlotsPerEpoch)).Add(1)
		header, err = light_client.NewWrappedHeader(&pb.LightClientHeaderCapella{
			Beacon: &pb.BeaconBlockHeader{
				Slot:          slot,
				ProposerIndex: primitives.ValidatorIndex(rand.Int()),
				ParentRoot:    sampleRoot,
				StateRoot:     sampleRoot,
				BodyRoot:      sampleRoot,
			},
			Execution: &enginev1.ExecutionPayloadHeaderCapella{
				ParentHash:       make([]byte, fieldparams.RootLength),
				FeeRecipient:     make([]byte, fieldparams.FeeRecipientLength),
				StateRoot:        make([]byte, fieldparams.RootLength),
				ReceiptsRoot:     make([]byte, fieldparams.RootLength),
				LogsBloom:        make([]byte, fieldparams.LogsBloomLength),
				PrevRandao:       make([]byte, fieldparams.RootLength),
				ExtraData:        make([]byte, 0),
				BaseFeePerGas:    make([]byte, fieldparams.RootLength),
				BlockHash:        make([]byte, fieldparams.RootLength),
				TransactionsRoot: make([]byte, fieldparams.RootLength),
				WithdrawalsRoot:  make([]byte, fieldparams.RootLength),
			},
			ExecutionBranch: sampleExecutionBranch,
		})
		require.NoError(t, err)
		blk, err = blocks.NewSignedBeaconBlock(util.NewBeaconBlockCapella())
		require.NoError(t, err)
	case version.Deneb:
		slot = primitives.Slot(config.DenebForkEpoch * primitives.Epoch(config.SlotsPerEpoch)).Add(1)
		header, err = light_client.NewWrappedHeader(&pb.LightClientHeaderDeneb{
			Beacon: &pb.BeaconBlockHeader{
				Slot:          slot,
				ProposerIndex: primitives.ValidatorIndex(rand.Int()),
				ParentRoot:    sampleRoot,
				StateRoot:     sampleRoot,
				BodyRoot:      sampleRoot,
			},
			Execution: &enginev1.ExecutionPayloadHeaderDeneb{
				ParentHash:       make([]byte, fieldparams.RootLength),
				FeeRecipient:     make([]byte, fieldparams.FeeRecipientLength),
				StateRoot:        make([]byte, fieldparams.RootLength),
				ReceiptsRoot:     make([]byte, fieldparams.RootLength),
				LogsBloom:        make([]byte, fieldparams.LogsBloomLength),
				PrevRandao:       make([]byte, fieldparams.RootLength),
				ExtraData:        make([]byte, 0),
				BaseFeePerGas:    make([]byte, fieldparams.RootLength),
				BlockHash:        make([]byte, fieldparams.RootLength),
				TransactionsRoot: make([]byte, fieldparams.RootLength),
				WithdrawalsRoot:  make([]byte, fieldparams.RootLength),
			},
			ExecutionBranch: sampleExecutionBranch,
		})
		require.NoError(t, err)
		blk, err = blocks.NewSignedBeaconBlock(util.NewBeaconBlockDeneb())
		require.NoError(t, err)
	case version.Electra:
		slot = primitives.Slot(config.ElectraForkEpoch * primitives.Epoch(config.SlotsPerEpoch)).Add(1)
		header, err = light_client.NewWrappedHeader(&pb.LightClientHeaderDeneb{
			Beacon: &pb.BeaconBlockHeader{
				Slot:          slot,
				ProposerIndex: primitives.ValidatorIndex(rand.Int()),
				ParentRoot:    sampleRoot,
				StateRoot:     sampleRoot,
				BodyRoot:      sampleRoot,
			},
			Execution: &enginev1.ExecutionPayloadHeaderDeneb{
				ParentHash:       make([]byte, fieldparams.RootLength),
				FeeRecipient:     make([]byte, fieldparams.FeeRecipientLength),
				StateRoot:        make([]byte, fieldparams.RootLength),
				ReceiptsRoot:     make([]byte, fieldparams.RootLength),
				LogsBloom:        make([]byte, fieldparams.LogsBloomLength),
				PrevRandao:       make([]byte, fieldparams.RootLength),
				ExtraData:        make([]byte, 0),
				BaseFeePerGas:    make([]byte, fieldparams.RootLength),
				BlockHash:        make([]byte, fieldparams.RootLength),
				TransactionsRoot: make([]byte, fieldparams.RootLength),
				WithdrawalsRoot:  make([]byte, fieldparams.RootLength),
			},
			ExecutionBranch: sampleExecutionBranch,
		})
		require.NoError(t, err)
		blk, err = blocks.NewSignedBeaconBlock(util.NewBeaconBlockElectra())
		require.NoError(t, err)
	case version.Fulu:
		slot = primitives.Slot(config.FuluForkEpoch * primitives.Epoch(config.SlotsPerEpoch)).Add(1)
		header, err = light_client.NewWrappedHeader(&pb.LightClientHeaderDeneb{
			Beacon: &pb.BeaconBlockHeader{
				Slot:          slot,
				ProposerIndex: primitives.ValidatorIndex(rand.Int()),
				ParentRoot:    sampleRoot,
				StateRoot:     sampleRoot,
				BodyRoot:      sampleRoot,
			},
			Execution: &enginev1.ExecutionPayloadHeaderDeneb{
				ParentHash:       make([]byte, fieldparams.RootLength),
				FeeRecipient:     make([]byte, fieldparams.FeeRecipientLength),
				StateRoot:        make([]byte, fieldparams.RootLength),
				ReceiptsRoot:     make([]byte, fieldparams.RootLength),
				LogsBloom:        make([]byte, fieldparams.LogsBloomLength),
				PrevRandao:       make([]byte, fieldparams.RootLength),
				ExtraData:        make([]byte, 0),
				BaseFeePerGas:    make([]byte, fieldparams.RootLength),
				BlockHash:        make([]byte, fieldparams.RootLength),
				TransactionsRoot: make([]byte, fieldparams.RootLength),
				WithdrawalsRoot:  make([]byte, fieldparams.RootLength),
			},
			ExecutionBranch: sampleExecutionBranch,
		})
		require.NoError(t, err)
		blk, err = blocks.NewSignedBeaconBlock(util.NewBeaconBlockFulu())
		require.NoError(t, err)
	default:
		return nil, fmt.Errorf("unsupported version %s", version.String(v))
	}

	update, err := lightclient.CreateDefaultLightClientUpdate(blk)
	require.NoError(t, err)
	update.SetSignatureSlot(slot - 1)
	syncCommitteeBits := make([]byte, 64)
	syncCommitteeSignature := make([]byte, 96)
	update.SetSyncAggregate(&pb.SyncAggregate{
		SyncCommitteeBits:      syncCommitteeBits,
		SyncCommitteeSignature: syncCommitteeSignature,
	})

	require.NoError(t, update.SetAttestedHeader(header))
	require.NoError(t, update.SetFinalizedHeader(header))

	return update, nil
}

func saveHead(t *testing.T, ctx context.Context, d db.Database) {
	blk := util.NewBeaconBlock()
	blkRoot, err := blk.Block.HashTreeRoot()
	require.NoError(t, err)
	util.SaveBlock(t, ctx, d, blk)
	st, err := util.NewBeaconState()
	require.NoError(t, err)
	require.NoError(t, d.SaveState(ctx, st, blkRoot))
	require.NoError(t, d.SaveHeadBlockRoot(ctx, blkRoot))
}
